﻿using System;
using System.Text;
using System.Web.Configuration;

namespace HeroldIT.Dnn.Modules
{
    /// <summary>
    /// Represents a globally unique identifier <see cref="Guid"/> with a 
    /// shorter string value.
    /// </summary>
    public struct Sguid
    {
        #region static fields

        /// <summary>
        /// A read-only instance of the <see cref="Sguid" /> class whose value 
        /// is guaranteed to be all zeroes.
        /// </summary>
        public static readonly Sguid Empty = new Sguid(Guid.Empty);

        #endregion

        #region static properties

        private static byte? salt;

        private static byte Salt
        {
            get
            {
                if (!salt.HasValue)
                {
                    MachineKeySection configSection =
                              (MachineKeySection)WebConfigurationManager.GetSection("system.web/machineKey");
                    salt = (byte) ((byte)configSection.DecryptionKey.GetHashCode() % 64);
                }
                return salt.Value;
            }
        }

        private static char[] base64Table;

        private static char[] Base64Table
        {
            get
            {
                if (null == base64Table)
                {
                    base64Table =
                        new[]
                            {
                                'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L',
                                'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X',
                                'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j',
                                'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v',
                                'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7',
                                '8', '9', '+', '/'
                            };
                }
                return base64Table;
            }
        }

        #endregion

        #region static methods

        /// <summary>
        /// Initialises a new instance of the Sguid class
        /// </summary>
        /// <returns></returns>
        public static Sguid NewGuid()
        {
            return new Sguid(Guid.NewGuid());
        }

        private static char RotateChar(char c, byte amount)
        {
            byte bias;
            if (c >= 'A' && c <= 'Z')
            {
                bias = (byte) (c - 'A');
            }
            else if (c >= 'a' && c <= 'z')
            {
                bias = (byte) (c - 'a' + 26);
            }
            else if (c >= '0' && c <= '9')
            {
                bias = (byte) (c - '0' + 52);
            }
            else if (c == '+')
            {
                bias = 62;
            }
            else if (c == '/')
            {
                bias = 63;
            }
            else
            {
                throw new ArgumentOutOfRangeException("c");
            }
            if (amount > 63)
            {
                throw new ArgumentOutOfRangeException("amount");
            }
            return Base64Table[(bias + amount) % 64];
        }

        #region Encode

        /// <summary>
        /// Creates a new instance of a Guid using the string value, 
        /// then returns the base64 encoded version of the Guid.
        /// </summary>
        /// <param name="value">An actual Guid string (i.e. not a Sguid)</param>
        /// <returns></returns>
        public static string Encode(string value)
        {
            Guid guid = new Guid(value);
            return Encode(guid);
        }

        /// <summary>
        /// Encodes the given Guid as a base64 string that is 22 
        /// characters long.
        /// </summary>
        /// <param name="guid">The Guid to encode</param>
        /// <returns></returns>
        public static string Encode(Guid guid)
        {
            StringBuilder encoded = new StringBuilder(22);
            foreach (char c in Convert.ToBase64String(guid.ToByteArray()).Substring(0, 22))
            {
                encoded.Append(RotateChar(c, Salt));
            }
            
            encoded = encoded
                .Replace("/", "_")
                .Replace("+", "-");
            return encoded.ToString();
        }

        #endregion

        #region Decode

        /// <summary>
        /// Decodes the given base64 string
        /// </summary>
        /// <param name="value">The base64 encoded string of a Guid</param>
        /// <returns>A new Guid</returns>
        public static Guid Decode(string value)
        {
            StringBuilder decoded = new StringBuilder(22);
            foreach (char c in value.Replace("_", "/").Replace("-", "+"))
            {
                decoded.Append(RotateChar(c, (byte) ((64 - Salt) % 64)));
            }

            byte[] buffer = Convert.FromBase64String(decoded + "==");
            return new Guid(buffer);
        }

        #endregion

        #endregion

        #region operators

        /// <summary>
        /// Determines if both ShortGuids have the same underlying 
        /// Guid value.
        /// </summary>
        /// <param name="x"></param>
        /// <param name="y"></param>
        /// <returns></returns>
        public static bool operator ==(Sguid x, Sguid y)
        {
            if ((object)x == null) return (object)y == null;
            return x.guid == y.guid;
        }

        /// <summary>
        /// Determines if both ShortGuids do not have the 
        /// same underlying Guid value.
        /// </summary>
        /// <param name="x"></param>
        /// <param name="y"></param>
        /// <returns></returns>
        public static bool operator !=(Sguid x, Sguid y)
        {
            return !(x == y);
        }

        /// <summary>
        /// Implicitly converts the Sguid to it's string equivilent
        /// </summary>
        /// <param name="sguid"></param>
        /// <returns></returns>
        public static implicit operator string(Sguid sguid)
        {
            return sguid.value;
        }

        /// <summary>
        /// Implicitly converts the Sguid to it's Guid equivilent
        /// </summary>
        /// <param name="sguid"></param>
        /// <returns></returns>
        public static implicit operator Guid(Sguid sguid)
        {
            return sguid.guid;
        }

        /// <summary>
        /// Implicitly converts the string to a Sguid
        /// </summary>
        /// <param name="shortGuid"></param>
        /// <returns></returns>
        public static implicit operator Sguid(string shortGuid)
        {
            return new Sguid(shortGuid);
        }

        /// <summary>
        /// Implicitly converts the Guid to a Sguid 
        /// </summary>
        /// <param name="guid"></param>
        /// <returns></returns>
        public static implicit operator Sguid(Guid guid)
        {
            return new Sguid(guid);
        }

        #endregion

        #region fields

        private Guid guid;
        private readonly string value;

        #endregion

        #region properties

        /// <summary>
        /// Gets/sets the underlying Guid
        /// </summary>
        public Guid Guid
        {
            get { return this.guid; }
        }

        /// <summary>
        /// Gets/sets the underlying base64 encoded string
        /// </summary>
        public string Value
        {
            get { return this.value; }
        }

        #endregion

        #region contructors

        /// <summary>
        /// Creates a Sguid from a base64 encoded string
        /// </summary>
        /// <param name="value">The encoded guid as a 
        /// base64 string</param>
        public Sguid(string value)
        {
            this.value = value;
            this.guid = value.Length == 22 ? Decode(value) : new Guid(value);
        }

        /// <summary>
        /// Creates a Sguid from a Guid
        /// </summary>
        /// <param name="guid">The Guid to encode</param>
        public Sguid(Guid guid)
        {
            this.value = Encode(guid);
            this.guid = guid;
        }

        #endregion

        #region methods

        /// <summary>
        /// Returns the base64 encoded guid as a string
        /// </summary>
        /// <returns></returns>
        public override string ToString()
        {
            return this.value;
        }

        /// <summary>
        /// Returns a value indicating whether this instance and a 
        /// specified Object represent the same type and value.
        /// </summary>
        /// <param name="obj">The object to compare</param>
        /// <returns></returns>
        public override bool Equals(object obj)
        {
            if (obj is Sguid)
                return this.guid.Equals(((Sguid)obj).guid);
            if (obj is Guid)
                return this.guid.Equals((Guid)obj);
            if (obj is string)
                return this.guid.Equals(((Sguid)obj).guid);
            return false;
        }

        /// <summary>
        /// Returns the HashCode for underlying Guid.
        /// </summary>
        /// <returns></returns>
        public override int GetHashCode()
        {
            return this.guid.GetHashCode();
        }

        #endregion
    }
}